﻿//Copyright (c) 2010 Christopher E. S. King (http://chriskingconsulting.com)
//
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 
// (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, 
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Collections;
using System.Diagnostics.Contracts;
using System.Reflection;
using System.Xml.Linq;
using System.Text.RegularExpressions;
using System.Globalization;
using System.Collections.Specialized;
using SysDirectory = System.IO.Directory;

using Microsoft.Build.Utilities;

using King.Text;
using King.Extensions;
using King.Xml;
using King.Reflection;
using King.Declarative;
using System.Diagnostics;
using System.Text;

namespace King.Automation {

    #region Attributes
    public abstract class CommandBaseAttribute : Attribute {
        public char Delimiter { get; set; }
    }

    [AttributeUsage(AttributeTargets.Assembly)]
    public sealed class CommandAssemblyAttribute : CommandBaseAttribute {

        public const ConsoleColor DefaultForegroundColor = ConsoleColor.Gray;
        public const ConsoleColor DefaultBackgroundColor = ConsoleColor.Black;

        private ConsoleColor? m_foregroundColor;
        private ConsoleColor? m_backgroundColor;

        public ConsoleColor ForegroundColor {
            set { m_foregroundColor = value; }
            get { 
                return m_foregroundColor == null ? 
                    DefaultForegroundColor : 
                    m_foregroundColor.Value; 
            }
        }
        public ConsoleColor BackgroundColor {
            set { m_backgroundColor = value; }
            get { 
                return m_backgroundColor == null ? 
                    DefaultBackgroundColor :
                    m_backgroundColor.Value; 
            }
        }
    }

    [AttributeUsage(AttributeTargets.Class, Inherited = false)]
    public sealed class CommandGroupAttribute : CommandBaseAttribute {
        private string m_name;

        public CommandGroupAttribute() {
        }
        public CommandGroupAttribute(string name) {
            m_name = name;
        }

        public string Name {
            get { return m_name; }
        }
    }

    [AttributeUsage(AttributeTargets.Constructor)]
    public sealed class CommandAttribute : CommandBaseAttribute {
        private string m_requiredSwitch;

        public CommandAttribute() {
        }
        public CommandAttribute(string requiredSwitch) {
            m_requiredSwitch = requiredSwitch;
        }

        public string RequiredSwitch {
            get { return m_requiredSwitch; }
        }
        public bool IsDefault { get; set; }
    }

    public abstract class CommandParameterAttribute : Attribute {

        /// <summary>
        /// Whether this parameter is a path, if it's a file or directory, if it exists, and if it's
        /// relative to the working directory.
        /// </summary>
        public CommandPathInfo Path { get; set; }
        public int RelativeTo { get; set; }
        public object DefaultValue { get; set; }
        public char Delimiter { get; set; }
    }

    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
    public sealed class CommandTargetAttribute : CommandParameterAttribute {
        private string m_documentationName;

        public CommandTargetAttribute() {
        }
        public CommandTargetAttribute(string documentationName) {
            m_documentationName = documentationName;
        }

        public string DocumentationName {
            get { return m_documentationName; }
        }
    }

    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
    public sealed class CommandSwitchAttribute : CommandParameterAttribute {
        private string m_documentationValue;

        public CommandSwitchAttribute() {
        }
        public CommandSwitchAttribute(string documentationValue) {
            m_documentationValue = documentationValue;
        }

        public string DocumentationValue {
            get { return m_documentationValue; }
        }
    }
    #endregion

    public enum TextPathAge {
        Older = -1,
        Same = 0,
        Newer = 1,
        First = 2,
    }
    public sealed class TextPath : TextDeclaration, IComparable<TextPath> {

        public static implicit operator string(TextPath path) {
            return path.ReadToEnd();
        }
        public static implicit operator TextPath(string path) {
            return new TextPath { path };
        }

        private bool m_normalizeDirectory;
        private bool m_mustBeFile;
        private bool m_mustExist;
        private string m_fileName;
        private string m_path;

        public TextPath(
            bool isFile = false, 
            bool isDirectory = false, 
            bool mustExist = false,
            string fileName = null) {

            m_normalizeDirectory = isDirectory;
            m_mustBeFile = isFile;
            m_mustExist = mustExist;
            m_fileName = fileName;
        }

        protected override IEnumerable Children(DeclarationContext frame) {
            var components = frame.Flatten(Children()).Select(o => frame.ToString(o)).WhereNotDefault().ToArray();
            var path = Path.Combine(components);

            // ensure directories end with a slash
            if (m_normalizeDirectory)
                path = path.NormalizeDirectory();

            // ensure a directory is not passed where we expect a file
            if (m_mustBeFile && path.IsDirectory())
                throw new ArgumentException("Not a file: " + path);

            if (m_mustExist) {

                // ensure exist exist
                if (m_mustBeFile && !File.Exists(path))
                    throw new ArgumentException("File not found: " + path);

                // ensure directory exists
                else if (m_normalizeDirectory && !SysDirectory.Exists(path))
                    throw new ArgumentException("Directory not found: " + path);

                // ensure exist or directory exists
                else if (!File.Exists(path) && !SysDirectory.Exists(path))
                    throw new ArgumentException("Directory or file not found: " + path);
            }

            // default filename
            if (path.IsDirectory() && m_fileName != null)
                path += m_fileName;

            yield return path;
        }
        protected override IEnumerable Substance() {
            return Children();
        }

        public Uri Url {
            get { return new Uri(Value, UriKind.RelativeOrAbsolute); }
        }
        public Uri DirectoryUrl {
            get { return Url.GetDirectory(); }
        }
        public string Value {
            get { 
                if (m_path == null)
                    m_path = ReadToEnd(); 
                return m_path;
            }
        }
        public TextPath MakeRelative(TextPath source) {
            return Value.ToUri().MakeRelativePath(source);
        }
        public TextPath MakeAbsolute(TextPath root) {
            return Value.MakeFullPath(root);
        }
        public TextPath Directory {
            get { return Path.GetDirectoryName(Value).NormalizeDirectory(); }
        }
        public TextPath Extension {
            get { return Path.GetExtension(Value); }
        }
        public TextPath FileName {
            get { return Path.GetFileName(Value); }
        }
        public bool IsFile {
            get { return !IsDirectory; }
        }
        public bool IsDirectory {
            get { return Value.IsDirectory(); }
        }
        public bool Exists {
            get { return IsFile ? File.Exists(this) : SysDirectory.Exists(this); }
        }
        public DateTime LastWriteTime {
            get {
                if (!Exists)
                    throw new ArgumentException("File not found: " + this);

                return File.GetLastWriteTime(this);
            }
        }
        public TextPathAge CompareAgeTo(TextPath target) {
            if (!target.Exists)
                return TextPathAge.First;

            var sourceCreation = LastWriteTime;
            var targetCreation = target.LastWriteTime;

            var compare = sourceCreation.CompareTo(targetCreation);

            if (compare == 0)
                return TextPathAge.Same;

            if (compare < 0)
                return TextPathAge.Older;

            return TextPathAge.Newer;
        }
        public bool Contains(TextPath sourceDir) {
            return sourceDir.Url.IsSubUrlOrEqual(Url);
        }

        public override string ToString() {
            return Value;
        }
        public override int GetHashCode() {
            return Url.GetHashCode();
        }
        public override bool Equals(object obj) {
            var rhs = obj as TextPath;
            if (rhs == null)
                return false;
            return Url.Equals(rhs.Url);
        }

        public int CompareTo(TextPath other) {
            return Value.ToLower().CompareTo(other.Value.ToLower());
        }

    }

    public sealed class XCommand {
        internal sealed class Tag { }
        public static readonly XName Console = "console";
    }

    public abstract class CommandException : Exception {
        internal CommandException(string message) : base(message) { }
    }
    public class CommandBindException : CommandException {
        internal CommandBindException(string message) : base(message) { }
    }
    public class CommandLoadException : CommandException {
        internal CommandLoadException(string message) : base(message) { }
    }
    public class CommandParseException : CommandException {
        internal CommandParseException(string message) : base(message) { }
    }

    public sealed class CommandAssemblyInfo {

        public static CommandAssemblyInfo GetExecutingAssembly() {
            return GetAssembly(Assembly.GetCallingAssembly());
        }
        public static CommandAssemblyInfo GetAssembly(Assembly assembly) {
            CommandAssemblyInfo cached;

            if (s_assemblyToCommandAssembly == null)
                s_assemblyToCommandAssembly = new Dictionary<Assembly, CommandAssemblyInfo>();

            if (!s_assemblyToCommandAssembly.TryGetValue(assembly, out cached))
                s_assemblyToCommandAssembly[assembly] = cached = new CommandAssemblyInfo(assembly);

            return cached;
        }

        internal static Dictionary<string, string> GetEnvironment() {

            var environment = new Dictionary<string, string>();

            // remap environment to a dictionary
            var env = Environment.GetEnvironmentVariables();
            foreach (var key in env.Keys)
                environment.Add((string)key, (string)env[key]);
            environment["cd"] = Environment.CurrentDirectory;

            return environment;
        }

        private static Dictionary<Assembly, CommandAssemblyInfo> s_assemblyToCommandAssembly;

        private Assembly m_assembly;
        private Dictionary<string, CommandGroupInfo> m_groupsByName;
        private Dictionary<Type, CommandGroupInfo> m_groupsByType;
        private Dictionary<RuntimeMethodHandle, CommandInfo> m_infosByMethodhandle;
        private string m_company, m_description, m_version, m_copyright;
        private char m_delimiter;
        private Command m_helpCommand;
        private ConsoleColor m_foregroundColor, m_backgroundColor;

        internal CommandAssemblyInfo(Assembly assembly) {
            Contract.Requires(assembly.IsDefined<AssemblyCompanyAttribute>());
            Contract.Requires(assembly.IsDefined<AssemblyDescriptionAttribute>());
            Contract.Requires(assembly.IsDefined<AssemblyVersionAttribute>());
            Contract.Requires(assembly.IsDefined<AssemblyCopyrightAttribute>());

            m_assembly = assembly;

            m_company = m_assembly.GetAttribute<AssemblyCompanyAttribute>().Company;
            if (string.IsNullOrEmpty(m_company))
                throw LoadException("must attribute a company by attributing its assembly with AssemblyCompanyAttribute.");

            m_description = m_assembly.GetAttribute<AssemblyDescriptionAttribute>().Description;
            if (string.IsNullOrEmpty(m_description))
                throw LoadException("must provide a description by attributing its assembly with AssemblyDescriptionAttribute.");

            m_version = m_assembly.GetAttribute<AssemblyFileVersionAttribute>().Version;
            if (string.IsNullOrEmpty(m_version))
                throw LoadException("must specify a company by attributing its assembly with AssemblyFileVersionAttribute.");

            m_copyright = m_assembly.GetAttribute<AssemblyCopyrightAttribute>().Copyright;
            if (string.IsNullOrEmpty(m_copyright))
                throw LoadException("must supply a copyright by attributing its assembly with AssemblyCopyrightAttribute.");

            m_foregroundColor = CommandAssemblyAttribute.DefaultForegroundColor;
            m_backgroundColor = CommandAssemblyAttribute.DefaultBackgroundColor;

            var attr = m_assembly.GetAttribute<CommandAssemblyAttribute>();
            m_delimiter = ',';
            if (attr != null) {
                if (attr.Delimiter != '\0')
                    m_delimiter = (char)attr.Delimiter;

                m_foregroundColor = attr.ForegroundColor;
                m_backgroundColor = attr.BackgroundColor;
            }
        }
        private T Exception<T>(string message) where T : CommandException {
            return (T)typeof(T).Activate(string.Format("Assembly {0} {1}", Name, message));
        }
        private CommandLoadException LoadException(string message) {
            return Exception<CommandLoadException>(message);
        }
        private void Initialize() {
            if (m_groupsByName != null)
                return;

            // types attributed with [CommandGroup]
            var groups =
               (from type in m_assembly.GetTypes()
                where type.IsDefined<CommandGroupAttribute>() && type.IsSubclassOf(typeof(Command))
                select type).ToArray();

            // ctors attributed with [Command]
            var ctors =
               (from type in m_assembly.GetTypes()
                from method in type.Get<ConstructorInfo>(declaredOnly: true)
                where method.IsDefined<CommandAttribute>()
                select method)
                .ToArray();

            // activate CommandInfos and cache them by their Type
            m_infosByMethodhandle = ctors.Select(o => new CommandInfo(this, o))
                .ToDictionary(o => o.Method.MethodHandle);

            // methods attributed with [Command] declared on types 
            // that do not derive from a type attributed with [CommandGroup]
            var singletons =
               (from type in m_infosByMethodhandle.Values.Select(o => o.Type)
                where !groups.Any(o => type.IsSubclassOfOrIs(o))
                select type).ToArray();

            // activate groups which are types attributed with [CommandGroup] 
            // and methods that are declared on stand alone commands
            m_groupsByName =
                groups.Concat(singletons)
                .Select(o => new CommandGroupInfo(this, o))
                .ToCaseInsensitiveDictionary(o => o.Name);

            m_groupsByType =
                m_groupsByName.Values
                .ToDictionary(o => o.Type);
        }

        public ConsoleColor ForegroundColor {
            get { return m_foregroundColor; }
        }
        public ConsoleColor BackgroundColor {
            get { return m_backgroundColor; }
        }

        public string Company {
            get { return m_company; }
        }
        public string Description {
            get { return m_description; }
        }
        public string Version {
            get { return m_version; }
        }
        public string Copyright {
            get { return m_copyright; }
        }
        public string Name {
            get { return m_assembly.GetName().Name; }
        }

        public Assembly Assembly {
            get { return m_assembly; }
        }
        public Command HelpCommand {
            get { 
                if (m_helpCommand == null)
                    m_helpCommand = new AssemblyHelpCommand(m_assembly.FullName);
                return m_helpCommand;
            }
        }
        public CommandGroupInfo GetGroup(Type type) {
            Initialize();
            return type
                .ToLinkList(o => o.BaseType)
                .Select(o => m_groupsByType.GetValueOrDefault(o))
                .WhereNotDefault()
                .FirstOrDefault();
        }
        public CommandGroupInfo GetGroup(string name) {
            Initialize();
            return m_groupsByName.GetValueOrDefault(name);
        }
        public IEnumerable<CommandGroupInfo> Groups() {
            Initialize();
            return m_groupsByName.Values;
        }
        public IEnumerable<CommandInfo> Infos(string name, string requiredSwitch = null) {
            Initialize();
            var group = m_groupsByName.GetValueOrDefault(name);
            if (group == null)
                return Enumerable.Empty<CommandInfo>();

            return group.Infos(requiredSwitch);
        }
        public IEnumerable<CommandInfo> Infos() {
            Initialize();
            return m_infosByMethodhandle.Values;
        }
        public CommandInfo GetInfo(RuntimeMethodHandle methodHandle) {
            return m_infosByMethodhandle.GetValueOrDefault(methodHandle);
        }
        public char Delimiter {
            get { return m_delimiter; }
        }

        public Command Bind(string commandLine) {
            return Bind(CommandLinePart.ParseLine(commandLine));
        }
        public Command Bind(string[] arguments) {
            return Bind(CommandLinePart.ParseLine(arguments));
        }
        public Command Bind(IEnumerable<CommandLinePart> arguments) {
            arguments = arguments.ToArray();

            if (arguments.None())
                return HelpCommand;

            if (arguments.Any() && arguments.First().Name == "?")
                return HelpCommand;

            var name = arguments.FirstOrDefault();
            if (name.IsSwitch)
                throw new ArgumentException("Command name cannot be a named argument.");

            var group = GetGroup(name.Value);
            if (group == null)
                throw new ArgumentException("Unrecognized command: " + name.Value);

            return group.Bind(arguments.Skip(1));
        }
    }
    public sealed class CommandGroupInfo {

        public static CommandGroupInfo GetGroup(Type type) {
            return CommandAssemblyInfo.GetAssembly(type.Assembly).GetGroup(type);
        }
        public static CommandGroupInfo GetGroup(string name) {
            var callingAssembly = System.Reflection.Assembly.GetCallingAssembly();
            return CommandAssemblyInfo.GetAssembly(callingAssembly).GetGroup(name);
        }

        private Type m_type;
        private CommandAssemblyInfo m_assembly;
        private string m_name;
        private IEnumerable<CommandInfo> m_infos;
        private ILookup<string, CommandInfo> m_infosByRequiredSwitch;
        private char m_delimiter;
        private Command m_helpCommand;

        internal CommandGroupInfo(CommandAssemblyInfo assembly, Type type) {
            m_assembly = assembly;
            m_type = type;
            m_delimiter = assembly.Delimiter;

            var attr = type.GetAttribute<CommandGroupAttribute>();
            if (attr != null) {
                if (attr.Name != null)
                    m_name = attr.Name.ToLower();
                if (attr.Delimiter != '\0')
                    m_delimiter = (char)attr.Delimiter;
            }

            if (m_name == null)
                m_name = m_type.Name.ReplaceEnd("Command").ToLowerFirst();
        }
        private void Initialize() {
            if (m_infos != null)
                return;

            m_infos = m_assembly.Infos().Where(o => o.Type.IsSubclassOfOrIs(m_type)).ToArray();
            m_infosByRequiredSwitch = m_infos.Where(o => o.RequiredSwitch != null).ToLookup(o => o.RequiredSwitch.ToLower());
        }

        public CommandAssemblyInfo Assembly {
            get { return m_assembly; }
        }
        public string Name {
            get { return m_name; }
        }
        public string Documentation {
            get { return m_type.GetDocumentation(); }
        }
        public Type Type {
            get { return m_type; }
        }
        public Command HelpCommand {
            get {
                if (m_helpCommand == null)
                    m_helpCommand = new GroupHelpCommand(m_type.AssemblyQualifiedName);
                return m_helpCommand;
            }
        }
        public IEnumerable<CommandInfo> Infos() {
            Initialize();
            return m_infos;
        }
        public IEnumerable<CommandInfo> Infos(string requiredSwitch = null) {
            Initialize();
            if (requiredSwitch == null)
                return m_infos.Where(o => o.RequiredSwitch == null);

            requiredSwitch = requiredSwitch.ToLower();
            if (!m_infosByRequiredSwitch.Contains(requiredSwitch))
                return Enumerable.Empty<CommandInfo>();

            return m_infosByRequiredSwitch[requiredSwitch];
        }
        public CommandInfo GetCommandInfo(string requiredSwitch = null) {
            return Infos(requiredSwitch).SingleOrDefault();
        }
        public CommandInfo GetCommandInfo(RuntimeMethodHandle methodHandle) {
            return Assembly.GetInfo(methodHandle);
        }

        public Command Bind(string commandLine) {
            return Bind(CommandLinePart.ParseLine(commandLine));
        }
        public Command Bind(string[] arguments) {
            return Bind(CommandLinePart.ParseLine(arguments));
        }
        public Command Bind(IEnumerable<CommandLinePart> arguments) {
            Exception exception;
            arguments = arguments.ToArray();

            // help if /? is present
            if (arguments.Any(o => o.Name == "?"))
                return HelpCommand;

            // bind to sub commands (if first argument looks like an switch without a value)
            var firstArgument = arguments.FirstOrDefault();
            if (firstArgument != null && firstArgument.IsSwitch && !firstArgument.HasValue) {

                var commandInfos = Infos(firstArgument.Name).ToArray();
                if (commandInfos.None())
                    throw new ArgumentException("Command '" + Name + "' has no sub commands '" + firstArgument.Name + "'.");

                if (commandInfos.OnlyOne()) {
                    var command = commandInfos.Single().Bind(arguments, out exception);

                    // throw specific binding exception in common case where there is only one variant
                    if (command == null)
                        throw exception;

                    return command;
                }
            }
            
            // throw less specific error when binding to none or more than one command
            var commands = Infos().Select(o => o.Bind(arguments, out exception)).WhereNotDefault().ToArray();
            if (commands.None())
                throw new ArgumentException("No variant of command '" + Name + 
                    "' could be bound given the arguments.");

            if (commands.MoreThanOne())
                throw new ArgumentException(
                    "More that one variant of command '" + Name +
                    "' could be bound given the arguments.");

            return commands.Single();
        }
        public char Delimiter {
            get { return m_delimiter; }
        }

        public override string ToString() {
            return m_name;
        }
    }
    internal sealed class CommandSignatureInfo {

        private Type m_type;
        private CommandAssemblyInfo m_assembly;
        private ConstructorInfo m_ctor;
        private IEnumerable<object> m_signatureParts;

        internal CommandSignatureInfo(CommandAssemblyInfo assembly, ConstructorInfo ctor) {
            m_assembly = assembly;
            m_ctor = ctor;
            m_type = ctor.DeclaringType;
        }

        private void Initialize() {
            if (m_signatureParts != null)
                return;

            m_signatureParts =
               (from o in m_ctor.GetParameters()
                select o.ParameterType.IsSubclassOf(typeof(CommandArgumentGroup)) ?
                    (object)new CommandSignatureInfo(
                        m_assembly,
                        o.ParameterType.Get<ConstructorInfo>(isInstance: true).Single()
                    ) : new CommandParameterInfo(this, o)).ToArray();
        }

        internal CommandAssemblyInfo Assembly {
            get { return m_assembly; }
        }
        internal Type Type {
            get { return m_type; }
        }
        internal MethodBase Method {
            get { return m_ctor; }
        }

        internal IEnumerable<CommandParameterInfo> Parameters() {
            Initialize();

            return m_signatureParts.SelectMany(o =>
                o is CommandParameterInfo ?
                    // leaf
                    new[] { (CommandParameterInfo)o } :

                    // node
                    ((CommandSignatureInfo)o).Parameters()
            );
        }
        internal IEnumerable<CommandParameterInfo> Switches() {
            return Parameters().Where(o => o.IsSwitch);
        }
        internal IEnumerable<CommandParameterInfo> Targets() {
            return Parameters().Where(o => o.IsTarget);
        }

        internal object Activate(IEnumerator flattenedArguments) {
            var arguments = new List<object>();
            var parameters = m_signatureParts.GetEnumerator();

            while (parameters.MoveNext()) {

                var parameter = parameters.Current;

                // leaf
                if (parameter is CommandParameterInfo) {
                    flattenedArguments.MoveNext();
                    arguments.Add(flattenedArguments.Current);
                }

                // node
                else {
                    var signature = (CommandSignatureInfo)parameter;
                    arguments.Add(signature.Activate(flattenedArguments));
                }
            }

            return m_ctor.InvokeOrActivate(arguments.ToArray());
        }
    }
    public sealed class CommandInfo {

        public static CommandInfo GetInfo(string name, string requiredSwitch = null) {
            var callingAssembly = System.Reflection.Assembly.GetCallingAssembly();
            var group = CommandAssemblyInfo.GetAssembly(callingAssembly).GetGroup(name);
            return group.GetCommandInfo(requiredSwitch);
        }
        public static CommandInfo GetInfo(Type type, string requiredSwitch = null) {
            var callingAssembly = System.Reflection.Assembly.GetCallingAssembly();
            var group = CommandAssemblyInfo.GetAssembly(callingAssembly).GetGroup(type);
            return group.GetCommandInfo(requiredSwitch);
        }

        private CommandGroupInfo m_group;
        private int? m_order;
        private string m_requiredSwitch;
        private char m_delimiter;
        private Func<object, object[], object> m_execute;
        private ConstructorInfo m_ctor;
        private CommandSignatureInfo m_signature;
        private Dictionary<string, CommandParameterInfo> m_parametersByName;

        internal CommandInfo(CommandAssemblyInfo assembly, ConstructorInfo ctor) {

            m_signature = new CommandSignatureInfo(assembly, ctor);
            m_ctor = ctor;

            var attr = ctor.GetAttribute<CommandAttribute>();
            
            if (attr.Delimiter != '\0')
                m_delimiter = (char)attr.Delimiter;
        }
        private void Initialize() {
            if (m_parametersByName != null)
                return;

            m_parametersByName = m_signature.Parameters().ToCaseInsensitiveDictionary(o => o.Name);

            // create signature Execute(DeclarationContext context, string arg0, ..., string argN)
            var types = new [] { typeof(DeclarationContext) }
                .Concat(Method.GetParameters().Select(o => o.ParameterType));
            m_execute = Dynamic.Bind("Execute", Type, types.ToArray());
        }

        private T Exception<T>(string message) where T : CommandException {
            return (T)typeof(T).Activate(string.Format("Command {0}.{1} {2}",
                Method.DeclaringType.FullName, Method.Name, message));
        }
        private CommandLoadException LoadException(string message) {
            return Exception<CommandLoadException>("failed to load because " + message);
        }
        
        internal IEnumerable Execute(object target, object[] arguments) {
            Initialize();

            // execute
            var result = m_execute(target, arguments);
            return (IEnumerable)result;
        }

        public CommandGroupInfo Group {
            get {
                if (m_group == null)
                    m_group = Assembly.Groups().Single(o => Type.IsSubclassOfOrIs(o.Type));
                return m_group; 
            }
        }

        public string Name {
            get { return Group.Name; }
        }
        public string RequiredSwitch {
            get {
                if (m_requiredSwitch == null) {
                    m_requiredSwitch = string.Empty;

                    var attr = m_ctor.GetAttribute<CommandAttribute>();

                    // possible required switch
                    if (!attr.IsDefault) {

                        // explict
                        if (attr.RequiredSwitch != null)
                            m_requiredSwitch = attr.RequiredSwitch;

                        // implicit 
                        else if (Group.Type != m_ctor.DeclaringType)
                            m_requiredSwitch = m_ctor.DeclaringType.Name.ReplaceEnd("Command").ReplaceStart(Group.Name.ToUpperFirst());
                    }

                }
                return m_requiredSwitch == string.Empty ? null : m_requiredSwitch.ToLowerFirst(); 
            }
        }
        public string Documentation {
            get { return Method.GetDocumentation(); }
        }
        public int? Order {
            get { return m_order; }
        }
        public char Delimiter {
            get {
                if (m_delimiter == '\0')
                    m_delimiter = Group.Delimiter;
                return m_delimiter; 
            }
        }
        public CommandInfo HelpCommand {
            get { throw new NotImplementedException(); }
        }

        public CommandAssemblyInfo Assembly {
            get { return m_signature.Assembly; }
        }
        public Type Type {
            get { return m_signature.Type; }
        }
        public MethodBase Method {
            get { return m_signature.Method; }
        }
        public CommandParameterInfo GetParameter(string name) {
            return m_parametersByName.GetValueOrDefault(name);
        }

        public IEnumerable<CommandParameterInfo> Parameters() {
            Initialize();
            return m_signature.Parameters();
        }
        public IEnumerable<CommandParameterInfo> Switches() {
            return Parameters().Where(o => o.IsSwitch);
        }
        public IEnumerable<CommandParameterInfo> Targets() {
            return Parameters().Where(o => o.IsTarget);
        }

        public Command Bind(IEnumerable<CommandLinePart> arguments, out Exception exception) {
            exception = null;
            arguments = arguments.ToArray();
            object paramArgument = null;
            var environment = CommandAssemblyInfo.GetEnvironment();

            if (RequiredSwitch != null) {
                var requiredSwitch = arguments.SingleOrDefault(o => o.IsSwitch && o.NameLowered == RequiredSwitch.ToLower());
                if (requiredSwitch == null) {
                    exception = new ArgumentException("Missing required switch: " + RequiredSwitch);
                    return null;
                }
                arguments = arguments.ExceptFor(requiredSwitch);
            }

            // split target parameters by params/not-params
            var paramTarget = Targets().Where(o => o.IsParams).SingleOrDefault();
            var nonParamTargets = Targets().Where(o => !o.IsParams);

            // zip targets
            var targetArguments = arguments.Where(o => o.IsTarget);
            var padding = Enumerable.Repeat(default(CommandLinePart), nonParamTargets.Count());
            var targets = nonParamTargets.Zip(targetArguments.Concat(padding),
                (p, a) => new { 
                    parameter = p, 
                    argument = a ?? p.GetDefaultPart(environment)
                }
            );

            // missing targets
            var missingTargets = targets.Where(o => o.parameter.IsRequired && o.argument == null);
            if (missingTargets.Any()) {
                exception = new ArgumentException("Required target(s) are missing: " +
                    missingTargets.Select(o => o.parameter.Name).StringJoin(", "));
                return null;
            }

            // extra targets
            var extraTargets = targetArguments.Skip(nonParamTargets.Count());
            if (extraTargets.Any()) {
                if (paramTarget == null) {
                    exception = new ArgumentException("Too many targets specified. These are extra targets: " +
                        extraTargets.Select(o => o.Value).StringJoin(prefix: "'", separator: ", ", suffix: "'"));
                    return null;
                }

                paramArgument = extraTargets.Select(o => paramTarget.Bind(o)).ToArray(paramTarget.Type.GetElementType());
            } 
            
            // no param arguments supplied
            else if (paramTarget != null) 
                paramArgument = new string[] { };

            // join switches
            var switches =
               (from parameter in Switches()
                join argument in arguments on
                    parameter.Name.ToLower() equals argument.NameLowered into o
                let defaultPart = parameter.GetDefaultPart(environment)
                select new { 
                    parameter, 
                    arguments = o.Any() || defaultPart == null ? o.ToArray() : new[] { defaultPart }
                }).ToArray();

            #region Missing, Duplicate, Extra Switches
            // missing switches
            var missingSwitches = switches.Where(o => o.parameter.IsRequired && o.arguments.None());
            if (missingSwitches.Any()) {
                exception = new ArgumentException("Required parameters are missing: " +
                    missingSwitches.Select(o => o.parameter.Name).StringJoin(", "));
                return null;
            }

            // duplicate switches
            var duplicateSwitches = switches.Where(o => o.arguments.MoreThanOne());
            if (duplicateSwitches.Any()) {
                exception = new ArgumentException("Switches specified more than once: " +
                    missingSwitches.Select(o => o.parameter.Name).StringJoin(", "));
                return null;
            }

            // extra switches
            var extraSwitches = arguments.Where(o => o.IsSwitch).Except(switches.SelectMany(o => o.arguments));
            if (extraSwitches.Any()) {
                exception = new ArgumentException("Unrecognized switches: " +
                    extraSwitches.Select(o => o.Name).StringJoin(", "));
                return null;
            }
            #endregion

            // activate
            var orderedArguments = targets
                .Concat(switches.Select(o => new { 
                    o.parameter,
                    argument = o.arguments.SingleOrDefault()                         
                })).Select(o => o.parameter.Bind(o.argument)).ToArray();

            // tack on params array argument
            if (paramArgument != null)
                orderedArguments = orderedArguments.Concat(paramArgument).ToArray();

            return (Command)m_signature.Activate(orderedArguments.GetEnumerator());
        }

        public override string ToString() {
            return new TextJoin(" ") {
                Assembly.Name.ToLower(),
                Group.Name.ToLower(),
                new TextJoin(prefix: "/") { RequiredSwitch }
            }.ReadToEnd();
        }
    }
    public sealed class CommandParameterInfo {

        private CommandSignatureInfo m_command;
        private ParameterInfo m_parameterInfo;
        private Type m_type;
        private CommandPathInfo m_path;
        private object m_defaultValue;
        private int m_relativeTo;
        private char m_delimiter;
        private bool m_isParams;
        private bool m_isSwitch;
        private bool m_isOptional;
        private string m_documentation;
        private string m_documentationValue;
        private string m_documentationName;

        internal CommandParameterInfo(CommandSignatureInfo signature, ParameterInfo parameter) {
            m_command = signature;

            //m_delimiter = signature.Delimiter;

            m_parameterInfo = parameter;
            m_isSwitch = parameter.IsOptional;
            m_isParams = parameter.IsDefined<ParamArrayAttribute>();
            m_documentation = parameter.GetDocumentation();
            m_type = parameter.ParameterType;
            m_isOptional = m_parameterInfo.IsOptional || m_isParams;

            ICustomAttributeProvider cap = parameter;

            // harvest CommandParamterAttribute
            var attr = cap.GetAttribute<CommandParameterAttribute>();
            if (attr != null) {
                m_path = attr.Path;
                if (attr.Delimiter != '\0')
                    m_delimiter = (char)attr.Delimiter;

                var switchAttr = cap.GetAttribute<CommandSwitchAttribute>();
                if (switchAttr != null) {
                    m_isSwitch = true;
                    m_documentationValue = switchAttr.DocumentationValue;
                }

                var targetAttr = cap.GetAttribute<CommandTargetAttribute>();
                if (targetAttr != null) {
                    m_isSwitch = false;
                    m_documentationName = targetAttr.DocumentationName;
                }

                m_defaultValue = attr.DefaultValue;

                m_relativeTo = attr.RelativeTo;
            }
        }
        private T Exception<T>(string message) where T : CommandException {
            //return (T)typeof(T).Activate(string.Format("Parmater '{0}' on method '{1}' on command '{2}' {2}", 
            //    Name, m_method.Name, Method.Name, message));
            return (T)typeof(T).Activate(message);
        }
        private CommandLoadException LoadException(string message) {
            return Exception<CommandLoadException>(message);
        }
        private CommandBindException BindException(string message) {
            return Exception<CommandBindException>(message);
        }

        private object BindArgumentElement(object argument) {

            //// ensure that the empty string is replaced by null
            //if (argument is string && ((string)argument) == string.Empty)
            //    argument = null;

            //if (argument == null)
            //    return null;

            //// ensure the string(s) all end with a backslash
            //if (IsDirectory)
            //    argument = ((string)argument).NormalizeDirectory();

            //// make path absolute by using the current as the base path
            //if (IsRelativeToCurrentDirectory)
            //    argument = new Uri(new Uri(Environment.CurrentDirectory.NormalizeDirectory()), (string)argument).LocalPath;

            //// ensure directory exists
            //if (IsExistingDirectory && !Directory.Exists((string)argument))
            //    throw BindException("cannot be bound to " + argument + " because the directory does not exist.");

            //// ensure file or directory exists
            //if (IsExistingDirectoryOrFile && !Directory.Exists((string)argument) && !File.Exists((string)argument))
            //    throw BindException("cannot be bound to " + argument + " because the directory or file do not exist.");

            //// ensure file exists
            //if (IsExistingFile && !File.Exists((string)argument))
            //    throw BindException("cannot be bound to " + argument + " because the file does not exist.");

            //// enum's that are not flags must be one of the named value
            //if (Type.ElementTypeOrSelf.RuntimeType.IsEnum &&
            //    !Enum.GetValues(Type.ElementTypeOrSelf).Cast<object>().Any(o => o.Equals(argument)))
            //    throw BindException("cannot be bound to " + argument + " because that enum value has no name.");

            return argument;
        }

        internal CommandLinePart GetDefaultPart(Dictionary<string, string> environment) {
            var defaultValue = GetDefaultValue(environment);
            if (defaultValue == null)
                return null;
            return new CommandLinePart(Name) { defaultValue };
        }
        internal object GetDefaultValue(Dictionary<string, string> environment) {

            // assign default value
            var argument = DefaultValue;

            if (argument != null && argument is string) {
                
                // substitute environment variables into default values
                argument = Regex.Replace((string)argument, @"%\w*%",
                    delegate(Match match) {
                        var value = match.Value.Trim('%');
                        var env = environment.GetValueOrDefault(value);
                        return env;
                    }
                );

                // normalize empty string to null
                if (argument.Equals(string.Empty))
                    argument = null;
            }

            return argument;
        }
        internal object Bind(CommandLinePart argument) {
            if (argument == null)
                return Missing.Value;

            if (IsParams)
                return CommandLinePart.Convert(ElementType, argument.Value);

            if (IsArray)
                return argument.Values(ElementType, m_delimiter).ToArray(ElementType);

            return CommandLinePart.Convert(Type, argument.Value);
        }

        public Type Type {
            get { return m_type; }
        }
        public int Order {
            get { return m_parameterInfo.Position; }
        }

        public ParameterInfo ParameterInfo {
            get { return m_parameterInfo; }
        }

        public bool IsPath {
            get { return (m_path & CommandPathInfo.TypeMask) != 0; }
        }
        public bool IsDirectory {
            get { return IsPath && (m_path & CommandPathInfo.TypeMask) == CommandPathInfo.IsDirectory; }
        }
        public bool IsFile {
            get { return IsPath && (m_path & CommandPathInfo.TypeMask) == CommandPathInfo.IsFile; }
        }
        public bool IsExistingPath {
            get { return IsPath && (m_path & CommandPathInfo.Exists) != 0; }
        }
        public bool IsExistingDirectory {
            get { return IsExistingPath && IsDirectory; }
        }
        public bool IsExistingFile {
            get { return IsExistingPath && IsFile; }
        }
        public int IsRelativeTo {
            get { return m_relativeTo; }
        }
        public char Delimiter {
            get { return m_delimiter; }
        }

        public string DocumentationName {
            get { return m_documentationName ?? Name; }
        }
        public string DocumentationValue {
            get { return m_documentationValue; }
        }
        public string Documentation { 
            get { return m_documentation; }
        }
        public string Name {
            get { return m_parameterInfo.Name; }
        }
        public object DefaultValue {
            get {
                if (m_defaultValue != null)
                    return m_defaultValue;
                if (m_parameterInfo.DefaultValue is DBNull)
                    return null;
                return m_parameterInfo.DefaultValue; 
            }
        }

        public bool IsParams {
            get { return m_isParams; }
        }
        public bool IsArray {
            get { return m_type.IsArray; }
        }

        public bool IsOptional {
            get { return m_isOptional; }
        }
        public bool IsRequired {
            get { return !IsOptional; }
        }

        public bool IsSwitch {
            get { return m_isSwitch; }
        }
        public bool IsTarget {
            get { return !m_isSwitch; }
        }

        public Type ElementType {
            get { return m_type.HasElementType ? m_type.GetElementType() : m_type; }
        }
        public bool IsEnumeration {
            get { return m_type.IsEnumerable(); }
        }
        public bool IsNullable {
            get { return m_type.IsNullable(); }
        }
        public bool IsString {
            get { return ElementType == typeof(string); }
        }
        public bool IsEnum {
            get { return ElementType.IsEnum; }
        }

        public object ToObject(object argument) {
            //Contract.Requires(m_parameterInfo.ParameterType.CanAssignValue(argument));

            //// if argument is not an IEnumerable then initialize it else initialize each element
            //if (Type.IsEnumeration) {

            //    // verify no element is null
            //    if (((IEnumerable)argument).Cast<object>().Any(o => o == null))
            //        throw BindException("cannot be bound to an enumeration with a null or empty string element.");

            //} else {
            //    argument = BindArgumentElement(argument);
            //}

            //// basic type check
            //if (!m_parameterInfo.ParameterType.CanAssignValue(argument))
            //    throw BindException("cannot be bound to a value that cannot be stored in the parameter type.");

            //return argument;
            throw new NotImplementedException();
        }

        public override string ToString() {
            return (IsSwitch ? "/" : string.Empty) + Name;
        }
    }

    [Declaration(SubstituteCharacters = DeclarationAttribute.AllCharacters)]
    public sealed class CommandLinePart : TextDeclaration {

        #region Explict Operators
        public static explicit operator string(CommandLinePart argument) {
            return argument.Cast<string>();
        }
        public static explicit operator bool(CommandLinePart argument) {
            return argument.Cast<bool>();
        }
        public static explicit operator int(CommandLinePart argument) {
            return argument.Cast<int>();
        }
        public static explicit operator long(CommandLinePart argument) {
            return argument.Cast<long>();
        }
        public static explicit operator uint(CommandLinePart argument) {
            return argument.Cast<uint>();
        }
        public static explicit operator ulong(CommandLinePart argument) {
            return argument.Cast<ulong>();
        }
        public static explicit operator float(CommandLinePart argument) {
            return argument.Cast<float>();
        }
        public static explicit operator double(CommandLinePart argument) {
            return argument.Cast<double>();
        }
        public static explicit operator decimal(CommandLinePart argument) {
            return argument.Cast<decimal>();
        }
        public static explicit operator DateTime(CommandLinePart argument) {
            return argument.Cast<DateTime>();
        }
        public static explicit operator TimeSpan(CommandLinePart argument) {
            return argument.Cast<TimeSpan>();
        }
        public static explicit operator DateTimeOffset(CommandLinePart argument) {
            return argument.Cast<DateTimeOffset>();
        }
        public static explicit operator Guid(CommandLinePart argument) {
            return argument.Cast<Guid>();
        }

        public static explicit operator bool?(CommandLinePart argument) {
            return argument.Cast<bool?>();
        }
        public static explicit operator int?(CommandLinePart argument) {
            return argument.Cast<int?>();
        }
        public static explicit operator long?(CommandLinePart argument) {
            return argument.Cast<long?>();
        }
        public static explicit operator uint?(CommandLinePart argument) {
            return argument.Cast<uint?>();
        }
        public static explicit operator ulong?(CommandLinePart argument) {
            return argument.Cast<ulong?>();
        }
        public static explicit operator float?(CommandLinePart argument) {
            return argument.Cast<float?>();
        }
        public static explicit operator double?(CommandLinePart argument) {
            return argument.Cast<double?>();
        }
        public static explicit operator decimal?(CommandLinePart argument) {
            return argument.Cast<decimal?>();
        }
        public static explicit operator DateTime?(CommandLinePart argument) {
            return argument.Cast<DateTime?>();
        }
        public static explicit operator TimeSpan?(CommandLinePart argument) {
            return argument.Cast<TimeSpan?>();
        }
        public static explicit operator DateTimeOffset?(CommandLinePart argument) {
            return argument.Cast<DateTimeOffset?>();
        }
        public static explicit operator Guid?(CommandLinePart argument) {
            return argument.Cast<Guid?>();
        }
        #endregion

        internal static object Convert(Type type, string value) {

            if (type == typeof(string)) return value;

            if (type == typeof(bool)) return value == null ? true : bool.Parse(value);

            if (type == typeof(int)) return int.Parse(value);
            if (type == typeof(long)) return long.Parse(value);
            if (type == typeof(double)) return double.Parse(value);
            if (type == typeof(DateTime)) return DateTime.Parse(value);
            if (type == typeof(TimeSpan)) return TimeSpan.Parse(value);
            if (type == typeof(Guid)) return Guid.Parse(value);
            if (type == typeof(CommandNameValue)) return CommandNameValue.Parse(value);

            if (type == typeof(bool?)) return value != null ? 
                (bool?)Convert(typeof(bool), value) : true;

            if (type == typeof(int?)) return value != null ? (int?)Convert(typeof(int), value) : null;
            if (type == typeof(long?)) return value != null ? (long?)Convert(typeof(long), value) : null;
            if (type == typeof(double?)) return value != null ? (double?)Convert(typeof(double), value) : null;
            if (type == typeof(DateTime?)) return value != null ? (DateTime?)Convert(typeof(DateTime), value) : null;
            if (type == typeof(TimeSpan?)) return value != null ? (TimeSpan?)Convert(typeof(TimeSpan), value) : null;
            if (type == typeof(Guid?)) return value != null ? (Guid?)Convert(typeof(Guid), value) : null;

            if (type.IsEnum) return Enum.Parse(type, value, true);

            throw new NotSupportedException("Arguments of type '" + type.FullName + "' are not supported.");
        }

        public static IEnumerable<CommandLinePart> ParseLine(string commandLine) {
            var argument = new StringBuilder();
            var inQuotes = false;

            // unescape command line and yield arguments
            for (int i = 0; i < commandLine.Length; i++) {
                var character = commandLine[i];

                // handle quotes
                if (character == '"') {

                    // end quote
                    if (i == commandLine.Length - 1) {
                        if (!inQuotes)
                            throw new ArgumentException("Unmatched double quote.");
                        continue;
                    }

                    else {
                        character = commandLine[++i];

                        // if not a quote then toggle in/out of quote
                        if (character != '"')
                            inQuotes = !inQuotes;
                    }
                }

                // white space as argument delimiter
                if (char.IsWhiteSpace(character) && !inQuotes) {

                    // yield argument if any
                    if (argument.Length > 0)
                        yield return Parse(argument.ToString());
                    argument.Clear();

                    continue;
                }

                argument.Append(character);
            }

            // last argument
            if (argument.Length > 0)
                yield return Parse(argument.ToString());
        }
        public static IEnumerable<CommandLinePart> ParseLine(string[] commandLineParts) {
            foreach (var argument in commandLineParts)
                yield return Parse(argument);
        }
        private static CommandLinePart Parse(string commandLinePart) {
            var match = Regex.Match(commandLinePart, @"^/\s*(?<switch>[\w\?]*)\s*(:\s*(?<value>.*))?$");
            if (!match.Success)
                return new CommandLinePart { commandLinePart };

            var name = match.Get("switch");
            var value = match.Get("value");
            return new CommandLinePart(name) { value };
        }

        private string m_name;
        private string m_value;

        internal CommandLinePart(string name = null) {
            m_name = name;
        }

        public bool IsSwitch {
            get { return m_name != null; }
        }
        public bool IsTarget {
            get { return !IsSwitch; }
        }
        public bool HasValue {
            get { return Value != null; }
        }
        public string Name {
            get { return m_name; }
        }
        public string NameLowered {
            get { return m_name == null ? null : m_name.ToLower(); }
        }
        public string Value {
            get {
                if (m_value == null)
                    m_value = new TextJoin { base.Children() }.ReadToEnd();
                return m_value == string.Empty ? null : m_value;
            }
        }
        public IEnumerable<object> Values(Type type, char delimiter = ',') {
            var arrayType = type;
            return Values(delimiter).Select(o => Convert(arrayType, o));
        }
        public IEnumerable<T> Values<T>(char delimiter = ',') {
            return Values(typeof(T), delimiter).Cast<T>();
        }
        public IEnumerable<string> Values(char delimiter = ',') {
            if (!IsSwitch)
                throw new InvalidOperationException("Only named arguments may be arrays.");

            var value = Value;
            if (value == null)
                yield break;

            var result = new StringBuilder();
            for (int i = 0; i < value.Length; i++) {
                var character = value[i];

                if (character == '\\') {
                    if (value.Length - 1 == i)
                        throw new FormatException("Trailing slash not permitted in argument '" + value + "'.");

                    if (value[i + 1] == delimiter) {
                        character = value[++i];
                        result.Append(character);
                        continue;
                    }
                }

                if (character == delimiter) {
                    yield return result.ToString();
                    result.Clear();
                    continue;
                }

                result.Append(character);
            }

            yield return result.ToString();
        }
        public object Cast(Type type) {

            if (type == typeof(bool?)) {
                if (Name == null)
                    throw new NotSupportedException();

                if (Value == null) return null;
                if (Value == "true") return true;
                if (Value == "false") return false;

                throw new FormatException();
            }

            if (type == typeof(bool)) {
                var result = Cast(typeof(bool?));
                if (result == null)
                    return true;
                return result;
            }

            return Convert(type, Value);
        }
        public T Cast<T>() {
            return (T)Cast(typeof(T));
        }

        protected override IEnumerable Children(DeclarationContext frame) {
            var name = new TextJoin(prefix: '/') { m_name };
            if (!Declaration.IsSubstantial(base.Children()))
                return name;
            name = new TextJoin(suffix: ':') { name };

            var hasWhitespace = false;
            var stringBuilder = new StringBuilder();

            // test for whitespace by buffering all output
            foreach (var character in frame.ToCharacters(base.Children())) {
                stringBuilder.Append(character);
                if (character == '"')
                    stringBuilder.Append(character);
                else if (char.IsWhiteSpace(character))
                    hasWhitespace = true;
            }

            // quote string if whitespace is found and escape double quotes
            var result = stringBuilder.ToString();
            if (hasWhitespace)
                result = '"' + result + '"';

            return new TextJoin(prefix: name) { result };
        }

        public override string ToString() {
            return new TextJoin(':') {
                new TextJoin(prefix: '/') { m_name },
                base.Children()
            }.ReadToEnd();
        }
    }

    [Flags]
    public enum CommandPathInfo {

        /// <summary>
        /// The path is a file. Only has effect if Exists is also passed.
        /// </summary>
        IsFile = 1,

        /// <summary>
        /// Ensure the path, if supplied, ends with a backslash.
        /// </summary>
        IsDirectory = 2,

        TypeMask = IsFile | IsDirectory,

        /// <summary>
        /// The file or directory must exist.
        /// </summary>
        Exists = 1 << 3,
    }

    public interface IEnvironment : IDictionary<string, string> { }

    [Declaration]
    public abstract class Command : TextDeclaration {
        private CommandInfo m_info;
        private object[] m_arguments;

        public Command(params object[] arguments) {

            // initialize m_info
            var group = CommandGroupInfo.GetGroup(GetType());
            m_info = new StackTrace().GetFrames()
                .Select(o => group.GetCommandInfo(o.GetMethod().MethodHandle))
                .WhereNotDefault()
                .First();

            m_arguments = arguments;

            // assert an argument for each parameter
            var parameters = m_info.Parameters().ToArray();
        }

        protected sealed override IEnumerable Children(DeclarationContext frame) {

            var environment = CommandAssemblyInfo.GetEnvironment();

            // initialize arguments
            var dummyRootGroup = new CommandArgumentGroup(m_arguments);
            dummyRootGroup.Initialize(environment, m_info.Parameters().GetEnumerator());

            // execute command
            return m_info.Execute(this, new object[] { frame }.Concat(dummyRootGroup.Values()).ToArray());
        }

        public CommandInfo GetInfo() {
            return m_info;
        }
        public override string ToString() {
            throw new NotImplementedException();
            //var commandLine = new TextJoin(" ") {

            //    // command name
            //    GetType().Name.SplitCamelCase().Skip(1).StringJoin().ToLower(),

            //    // switches
            //    from o in GetType().Get<PropertyInfo>(isInstance: true, isPublic: true)
            //    where o.IsDefined<CommandOptionAttribute>()
            //    select GetOptionValue(o),

            //    // targets
            //    from o in GetType().Get<PropertyInfo>(isInstance: true, isPublic: true)
            //    let target = o.GetAttribute<CommandTargetAttribute>()
            //    where target != null
            //    //orderby target.Order
            //    select o.GetValue(this)
            //}.ReadToEnd();

            //return commandLine;
        }
    }
    internal sealed class CommandArgument {
        private readonly object m_value;
        private object m_initializedValue;
        private CommandParameterInfo m_info;

        internal CommandArgument(object value) {
            m_value = value;
        }

        private string InitializePath(string argument, Dictionary<string, string> environment, IList<CommandArgument> arguments) {

            if (argument == null)
                return null;

            // by default the root path is the current directory
            var rootDirectory = environment["cd"];

            // but the root path can also be another argument
            if (m_info.IsRelativeTo > 0)
                rootDirectory = (TextPath)arguments[m_info.IsRelativeTo - 1].Value;

            // make all paths absolute
            var path = new TextPath(
                    isFile: m_info.IsFile,
                    isDirectory: m_info.IsDirectory,
                    mustExist: m_info.IsExistingPath
                ) { rootDirectory, argument };

            return path;
        }
        private object InitializeArgument(object argument, Dictionary<string, string> environment, IList<CommandArgument> arguments) {

            // assign default value
            if (argument == null)
                argument = m_info.GetDefaultValue(environment);

            // initialize path
            if (m_info.IsPath) {

                // array of paths
                if (m_info.IsArray)
                    argument = argument.ToEnumerable<string>()
                        .Select(path => InitializePath(path, environment, arguments));

                // single path
                else
                    argument = InitializePath((string)argument, environment, arguments);
            }

            return argument;
        }
        internal void Initialize(
            Dictionary<string, string> environment,
            IList<CommandArgument> arguments,
            IEnumerator<CommandParameterInfo> infos) {

            // node
            if (m_value is CommandArgumentGroup) {
                var group = (CommandArgumentGroup)m_value;
                group.Initialize(environment, infos);

            // leaf
            } else {
                infos.MoveNext();

                m_info = infos.Current;
                m_initializedValue = InitializeArgument(m_value, environment, arguments);
            }
        }

        internal object Value {
            get {
                if (m_value is CommandArgumentGroup)
                    return m_value;
                return m_initializedValue;
            }
        }
        internal T Get<T>() {
            return (T)Value;
        }
    }
    public class CommandArgumentGroup {
        private IList<CommandArgument> m_arguments;
        private Dictionary<string, CommandArgument> m_parameters;

        internal protected CommandArgumentGroup(params object[] arguments) {
            m_arguments = arguments.Select(o => new CommandArgument(o)).ToArray();
            
            var constructor = (ConstructorInfo)new StackTrace().Methods().First(o => o.DeclaringType == GetType());
            m_parameters = constructor.GetParameters()
                .Zip(m_arguments, (p, a) => new { p, a })
                .ToCaseInsensitiveDictionary(o => o.p.Name, o => o.a);
        }
        internal void Initialize(
            Dictionary<string, string> environment,
            IEnumerator<CommandParameterInfo> infos) {

            foreach (var argument in m_arguments)
                argument.Initialize(environment, m_arguments, infos);            
        }

        internal IEnumerable<CommandArgument> Arguments() {
            return m_arguments;
        }
        internal IEnumerable<object> Values() {
            return m_arguments.Select(o => o.Value);
        }

        protected T Get<T>(string name) {
            return m_parameters[name].Get<T>();
        }
        protected T Get<T>() {
            var method = new StackTrace().Methods().Second();
            
            var name = method.Name;
            if (method.IsSpecialName)
                name = method.GetProperty().Name;

            return Get<T>(name);
        }
    }
    public sealed class CommandNameValue {
        internal static object Parse(string target) {
            var regex = Regex.Match(target, "^(?<name>[^=]+)=(?<value>.*)$");
            var name = regex.Get("name");
            var value = regex.Get("value");

            return new CommandNameValue(name, value);
        }

        private string m_name;
        private string m_value;

        public CommandNameValue(string name, string value) {
            m_name = name;
            m_value = value;
        }

        public string Name {
            get { return m_name; }
        }
        public string Value {
            get { return m_value; }
        }

        public override string ToString() {
            return m_name + "=" + m_value;
        }
    }
}